package org.groovymud.object.registry;

import java.io.IOException;
import java.util.Collection;
import java.util.Collections;
import java.util.HashSet;
import java.util.Map;
import java.util.Set;
import java.util.WeakHashMap;

import org.apache.log4j.Logger;
import org.codehaus.groovy.control.CompilationFailedException;
import org.groovymud.engine.event.EventScope;
import org.groovymud.engine.event.MudEventListener;
import org.groovymud.engine.event.ScopedEvent;
import org.groovymud.engine.event.action.SaveEvent;
import org.groovymud.engine.event.messages.MessageEvent;
import org.groovymud.engine.event.observer.Observable;
import org.groovymud.engine.event.observer.Observer;
import org.groovymud.engine.event.system.MovementEvent;
import org.groovymud.object.Container;
import org.groovymud.object.MudObject;
import org.groovymud.object.ObjectLocation;
import org.groovymud.object.alive.Alive;
import org.groovymud.object.alive.Player;
import org.groovymud.object.room.Room;
import org.groovymud.shell.command.CommandInterpreter;
import org.groovymud.shell.telnetd.LoggingExtendedTerminalIO;
import org.groovymud.shell.telnetd.LoginShell;
import org.springframework.beans.BeansException;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.ApplicationContext;
import org.springframework.context.ApplicationContextAware;

/* Copyright 2008 Matthew Corby-Eaglen
 *
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License. 
 * You may obtain a copy of the License at
 *
 *   http://www.apache.org/licenses/LICENSE-2.0 
 *
 * Unless required by applicable law or agreed to in writing, software 
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 
 * See the License for the specific language governing permissions and 
 * limitations under the License. 
 */
/**
 * The registry is the "root" container for all mud objects all global scoped
 * events pass through this object, and in turn are (if necessary) passed down
 * to the child rooms and objects.
 * 
 * spring injects mudEngine, mudObjectAttendant, commandIterpreter
 * 
 * this object is spring applicationcontextaware and this will get injected by
 * spring automatically.
 */
public class Registry implements Container, Observer, ApplicationContextAware {

    private final static Logger logger = Logger.getLogger(Registry.class);

    private final Map<LoginShell, Player> activePlayerHandles;

    protected final InventoryHandler inventoryHandler;

    private final MudObjectAttendant mudObjectAttendant;

    private ApplicationContext applicationContext;

    private final CommandInterpreter commandInterpreter;

    @Autowired(required = true)
    public Registry(InventoryHandler handler, MudObjectAttendant attendant, CommandInterpreter interpreter) {
        this.inventoryHandler = handler;
        activePlayerHandles = Collections.synchronizedMap(new WeakHashMap<LoginShell, Player>());
        this.mudObjectAttendant = attendant;
        this.commandInterpreter = interpreter;
    }

    /**
     * registers a parameter with the registry, and re-links all the contents of
     * any containers.
     * 
     * This method has to set the registry param of the registered object
     * because persistence removes it
     * 
     * @param obj
     */
    public void register(MudObject obj) {
        obj.setRegistry(this);
        obj.setInterpreter(getCommandInterpreter());
        if (isContainerInstance(obj)) {
            Container con = castToContainer(obj);
            con.getInventoryHandler().setRegistry(this);
            con.getInventoryHandler().relinkContent(con);
        }

        if (obj.getId() != null) {
            MudObject old = getMudObject(obj.getId());
            if (old != null) {
                old.dest(false);
            }
        }
        if (isAliveInstance(obj)) {
            Alive alive = castToAlive(obj);
            if (alive.getTerminalOutput() == null) {
                alive.setTerminalOutput(new LoggingExtendedTerminalIO(null));
                LoggingExtendedTerminalIO io = (LoggingExtendedTerminalIO) alive.getTerminalOutput();
                if (io != null) {
                    io.setObject(alive);
                }
            }
        }
        addMudObject(obj.getId() == null ? obj.getName() : obj.getId(), obj);
        addMudObject(obj);

    }

    public void update(MudObject object) {
        if (getMudObjects().contains(object)) {
            removeMudObject(object);
            addMudObject(object);
        } else {
            throw new IllegalArgumentException("cannot update registry for " + object);
        }
    }

    protected Alive castToAlive(MudObject obj) {
        return (Alive) obj;
    }

    protected boolean isAliveInstance(MudObject obj) {
        return obj instanceof Alive;
    }

    protected boolean isContainerInstance(MudObject obj) {
        return obj instanceof Container;
    }

    public void addMudObject(String key, MudObject object) {
        getInventoryHandler().addMudObject(key, object);
        addMudObject(object);
        if (object instanceof Room) {
            object.setCurrentContainer(this);
            castToObservable(object).addObserver(this);
        }
    }

    protected Observable castToObservable(MudObject object) {
        return ((Observable) object);
    }

    public void addActivePlayer(LoginShell shell, Player player) {
        addActivePlayerHandle(shell, player);
        if (logger.isDebugEnabled()) {
            logger.debug("There are " + getActivePlayerHandles().size() + " connections active and " + (Runtime.getRuntime().freeMemory() / 1000) + " kBytes free");
        }
    }

    public void removeaActivePlayer(Player player) {
        getActivePlayerHandlesMap().values().remove(player);
    }

    public MudObject removeMudObject(MudObject object) {
        if (object instanceof Player) {
            removeaActivePlayer((Player) object);
        }
        return getInventoryHandler().removeMudObject(object);
    }

    public void addActivePlayerHandle(LoginShell shell, Player player) {
        getActivePlayerHandlesMap().put(shell, player);
    }

    public Player getPlayerByHandle(LoginShell handle) {
        return getActivePlayerHandlesMap().get(handle);
    }

    protected Map<LoginShell, Player> getActivePlayerHandlesMap() {
        return activePlayerHandles;
    }

    protected Observer castToObserver(Object o) {
        return ((Observer) o);
    }

    public void update(Observable object, ScopedEvent arg) {
        for (Object objs : getMudObjects()) {
            if (objs instanceof MudEventListener) {
                castToMudEventListener(objs).onBeforeMudEvent(arg);
            }
        }
        if (!arg.isConsumed()) {
            if (arg instanceof MovementEvent) {
                findNewContainer(arg);
            }
            if (arg instanceof SaveEvent) {
                doSaveEvent(arg);
            }

            for (Object objs : getMudObjects()) {
                if (objs instanceof MudEventListener) {
                    castToMudEventListener(objs).onMudEvent(arg);
                }
            }
        }
        if (!arg.isConsumed()) {
            for (Object objs : getMudObjects()) {
                if (objs instanceof MudEventListener) {
                    castToMudEventListener(objs).onAfterMudEvent(arg);
                }
            }
        }
    }

    public MudEventListener castToMudEventListener(Object objs) {
        return ((MudEventListener) objs);
    }

    protected void doSaveEvent(ScopedEvent arg) {
        SaveEvent ev = (SaveEvent) arg;
        getMudObjectAttendant().savePlayerData((Player) ev.getSource());
    }

    public void dest(MudObject object) {
        if (object instanceof Player) {
            fireLeavingMessage((Player) object);
        }
        if (object instanceof Container) {
            unregisterContents(((Container) object));
        }
        removeMudObject(object);
        if (object.getCurrentContainer() != null) {
            object.getCurrentContainer().removeMudObject(object);
        }
        if (object instanceof Player) {
            try {
                ((Player) object).getTerminalOutput().close();
            } catch (IOException e) {
                logger.error(e, e);
            }
        }
    }

    /**
     * removes all objects from any container and sub containers
     * 
     * eg. "dest" them
     * 
     * @param container
     */
    public void unregisterContents(Container container) {
        for (MudObject obj : container.getInventoryHandler().getMudObjects()) {
            // remove objects from the game, but not from the container
            if (obj instanceof Container) {
                unregisterContents(castToContainer(obj));
            }
            removeMudObject(obj);
        }
    }

    private Container castToContainer(MudObject obj) {
        return ((Container) obj);
    }

    protected void fireLeavingMessage(Player player) {
        MessageEvent leaves = new MessageEvent(EventScope.GLOBAL_SCOPE);
        leaves.setScopeMessage("[" + player.getName() + " leaves GroovyMud]");
        leaves.setSource(player);
        leaves.setSourceMessage("Thanks for visiting!!");
        ((Observable) player).fireEvent(leaves);
    }

    protected synchronized void findNewContainer(ScopedEvent arg) {
        MovementEvent event = (MovementEvent) arg;
        Alive movingObject = event.getMovingObject();
        Container foundContainer = (Container) getMudObject(event.getRoomLocation().getBeanId());
        if (foundContainer == null) {
            Exception anException = null;
            try {
                foundContainer = (Container) getMudObjectAttendant().load(event.getRoomLocation());
            } catch (CompilationFailedException e) {
                logger.error(e, e);
                anException = e;
            } finally {
                if (anException != null) {
                    try {
                        movingObject.getTerminalOutput().writeln(anException.getMessage());
                    } catch (IOException e) {
                        logger.error(e, e);
                    }
                }
            }
        }
        event.setFoundRoom(foundContainer);
    }

    public void clear() {
        inventoryHandler.clear();
    }

    public boolean contains(MudObject object) {
        return inventoryHandler.contains(object);
    }

    public boolean containsAll(Collection<?> c) {
        return inventoryHandler.containsAll(c);
    }

    public MudObjectAttendant getMudObjectAttendant() {
        return mudObjectAttendant;
    }

    public Map<String, Set<MudObject>> getMudObjectsMap() {
        return getInventoryHandler().getMudObjectsMap();
    }

    public HashSet<LoginShell> getActivePlayerHandles() {
        return new HashSet<LoginShell>(getActivePlayerHandlesMap().keySet());
    }

    public void addMudObject(MudObject object) {
        getInventoryHandler().addMudObject(object);
    }

    public MudObject getMudObject(String name) {
        return getInventoryHandler().getMudObject(name);
    }

    public Set<MudObject> getMudObjects(String name) {
        return getInventoryHandler().getMudObjects(name);
    }

    public Set<MudObject> getMudObjects() {
        return getInventoryHandler().getMudObjects();
    }

    public InventoryHandler getInventoryHandler() {
        return inventoryHandler;
    }

    public void setInventoryHandler(InventoryHandler handler) {
        throw new UnsupportedOperationException("Registry cannot have its inventory handler set");
    }

    public EventScope getScope() {
        return EventScope.GLOBAL_SCOPE;
    }

    public ApplicationContext getApplicationContext() {
        return applicationContext;
    }

    public void setApplicationContext(ApplicationContext applicationContext) throws BeansException {
        this.applicationContext = applicationContext;
    }

    public ObjectLocation getObjectLocation() {
        return null;
    }

    public void setScope(EventScope scope) {

    }

    public CommandInterpreter getCommandInterpreter() {
        return commandInterpreter;
    }

}
